TP-Link Archer路由器LAN RCE
TP-Link Archer系列路由器是普联(TP-Link)公司的无线路由产品。TP-Link Archer A7/C7 (AC1750)的MIPS架构、固件版本为190726的路由器,其tdpServer服务存在命令注入漏洞,LAN侧的未授权攻击者可利用漏洞以root权限进行任意代码执行。
该漏洞由Flashback团队(Pedro Ribeiro + Radek Domanski)在Pwn2Own Tokyo 2019活动中发现。
在TP-Link官网可下载对应版本固件:https://static.tp-link.com/2019/201908/20190816/Archer%20C7(US)_V5_190726.zip,尝试使用firmadyne工具运行固件:
但发现网络分配步骤失败:
进而运行时报错:
使用Qemu MIPS虚拟机加载固件运行,找到目标文件tdpServer并尝试执行:
发现执行出错。
分析固件仿真启动和执行tdpServer失败的原因,估计是因为路由器在启动中,部分调用资源在其他硬件flash中,因而缺少资源导致失败,最好的方式不过为购买一台TP-Link Archer A7进行试验了。
tdpServer守护进程在本地0.0.0.0接口的UPD 20002端口监听,为TP-Link移动应用程序对路由器建立控制提供基础,守护进程与应用程序之间通过使用带有加密payload的UDP数据包通信,逆向分析得到数据包格式如下:
数据包的格式决定了守护进程提供的服务,例如type为0,守护进行将提供tdpd服务,使用特定的TETHER_KEY哈希值的数据包进行回复,但此处与漏洞无关;type为0xf0, 守护进行将提供onemesh服务, onemesh在许多TP-Link新固件中均引入了该项新技术,具体可参考( https://www.tp-link.com/us/onemesh/compatibility/)。
设备启动后,函数调用顺序为:tdpd_pkt_handler_loop()(地址:0x40d164)->监听端口20002接收到数据传递->tpdp_pkt_parser()(地址:0x40cfe0),tpdp_pkt_parser()函数伪码如下:
undefined4 FUN_0040cfe0(int iParm1,int iParm2,int iParm3,int *piParm4,int param_5)
{
byte bVar1;
uint uVar2;
undefined2 uVar5;
__pid_t _Var3;
int iVar4;
uint uVar6;
char *pcVar7;
char *pcVar8;
int local_c8 [42];
ushort local_20 [2];
int local_1c;
int local_18;
if (iParm1 != 0) {
iVar4 = FUN_0040d600();
if (iParm2 < iVar4) {
FUN_00403734("tdpdServer.c:709","recvbuf length = %d, less than hdr\'s 16",iParm2);
return 0xffffffff;
}
iVar4 = FUN_0040d620(iParm1);
if (iVar4 < 1) {
pcVar7 = "tdpdServer.c:716";
pcVar8 = "tdp pkt is too big";
}
else {
FUN_00403734("tdpdServer.c:719","tdp pkt length is %d",iVar4);
iVar4 = FUN_0040c9d0(iParm1,iVar4);
if (iVar4 < 0) {
return 0xffffffff;
}
if (*(char *)(iParm1 + 1) == 0) {
local_20[0] = 0;
if (iParm1 != 0) {
if (iParm3 == 0) {
return 0xffffffff;
}
if (piParm4 != (int *)0x0) {
bVar1 = *(byte *)(iParm1 + 6);
uVar2 = 1;
if ((bVar1 & 0x10) == 0) {
uVar2 = ((uint)bVar1 << 0x1a) >> 0x1f;
}
uVar6 = (uint)bVar1 & 1;
if (uVar2 == 0) {
uVar6 = 0;
}
if (uVar6 == 0) {
pcVar7 = "tdpdServer.c:837";
pcVar8 = "TDP flag error";
}
else {
if (*(short *)(iParm1 + 2) == 2) {
local_1c = 0;
local_18 = 0;
iVar4 = pipe((int *)&stack0xffffffe4);
if (iVar4 < 0) {
if (0 < local_1c) {
close(local_1c);
}
if (local_18 < 1) {
return 0xffffffff;
}
close(local_18);
return 0xffffffff;
}
_Var3 = fork();
if (_Var3 == -1) {
return 0xffffffff;
}
if (_Var3 == 0) {
close(local_1c);
local_20[0] = FUN_0040b804((void *)(iParm3 + 0x10),param_5);
write(local_18,local_20,2);
write(local_18,(void *)(iParm3 + 0x10),(uint)local_20[0]);
close(local_18);
/* WARNING: Subroutine does not return */
exit(0);
}
close(local_18);
wait((void *)0x0);
read(local_1c,local_20,2);
read(local_1c,(void *)(iParm3 + 0x10),(uint)local_20[0]);
close(local_1c);
uVar5 = *(undefined2 *)(iParm1 + 2);
}
else {
local_20[0] = FUN_0040b7d8(iParm3 + 0x10);
uVar5 = *(undefined2 *)(iParm1 + 2);
}
*(undefined2 *)(iParm3 + 2) = uVar5;
*(byte *)(iParm3 + 6) = *(byte *)(iParm1 + 6) & 0xfe | 2;
*(undefined *)(iParm3 + 7) = 1;
*(ushort *)(iParm3 + 4) = local_20[0];
*(undefined4 *)(iParm3 + 8) = *(undefined4 *)(iParm1 + 8);
iVar4 = FUN_0040cb20(iParm3);
if (-1 < iVar4) {
*piParm4 = iVar4;
return 0;
}
pcVar7 = "tdpdServer.c:844";
pcVar8 = "TDP encode pkt error";
}
FUN_00403734(pcVar7,pcVar8);
}
}
return 0xffffffff;
}
if ((*(char *)(iParm1 + 1) == -0x10) && (DAT_0042f0f0 == 1)) {
if ((iParm1 != 0) &&
(((iParm3 != 0 && (piParm4 != (int *)0x0)) &&
(iVar4 = FUN_0040e074(local_c8,iParm2), iVar4 == 0)))) {
FUN_00403734("tdpdServer.c:883","recv ip is %x, my ip is %x",param_5,local_c8[0]);
if (param_5 == local_c8[0]) {
FUN_00403734("tdpdServer.c:886","Ignore onemesh tdp packet to myself...");
}
else {
FUN_00403734("tdpdServer.c:890","opcode %x, flags %x",(uint)*(ushort *)(iParm1 + 2),
(uint)*(byte *)(iParm1 + 6));
switch((uint)*(ushort *)(iParm1 + 2) - 1 & 0xffff) {
case 0:
if ((*(byte *)(iParm1 + 6) & 1) == 0) {
pcVar7 = "tdpdServer.c:904";
pcVar8 = "Invalid flags";
}
else {
iVar4 = FUN_00412ed4(iParm1,iParm2,iParm3,piParm4,param_5);
if (-1 < iVar4) {
return 0;
}
pcVar7 = "tdpdServer.c:898";
pcVar8 = "error processing probe request...";
}
break;
case 1:
if ((*(byte *)(iParm1 + 6) & 1) != 0) {
return 0;
}
pcVar7 = "tdpdServer.c:915";
pcVar8 = "Invalid flags";
break;
default:
pcVar7 = "tdpdServer.c:966";
pcVar8 = "Invalid operation!";
break;
case 3:
if ((*(byte *)(iParm1 + 6) & 1) == 0) {
pcVar7 = "tdpdServer.c:931";
pcVar8 = "Invalid flags";
}
else }
iVar4 = FUN_00414650(iParm1,iParm2,iParm3,piParm4,param_5);
if (-1 < iVar4) {
return 0;
}
pcVar7 = "tdpdServer.c:925";
pcVar8 = "error processing attach_master request...";
}
break;
case 5:
if ((*(byte *)(iParm1 + 6) & 1) != 0) {
return 0;
}
pcVar7 = "tdpdServer.c:942";
pcVar8 = "Invalid flags";
break;
case 6:
if ((*(byte *)(iParm1 + 6) & 1) == 0) {
pcVar7 = "tdpdServer.c:958";
pcVar8 = "Invalid flags";
}
else {
iVar4 = FUN_00414d14(iParm1,iParm2,iParm3,piParm4,param_5);
if (-1 < iVar4) {
return 0;
}
pcVar7 = "tdpdServer.c:952";
pcVar8 = "error processing slave_key_offer request...";
}
}
FUN_00403734(pcVar7,pcVar8);
}
}
return 0xffffffff;
}
pcVar7 = "tdpdServer.c:742";
pcVar8 = "invalid tdp packet type";
}
FUN_00403734(pcVar7,pcVar8);
}
return 0xffffffff;
}
第一部分:检查数据包、校验和的验证。
第二部分:type判断,即调用服务选择。
第三部分:标志字段onemesh_flag为1,进入onemesh_main()(地址:0x40cd78),onemesh_main()根据操作码字段调用相应函数。
举例:操作码为6——调用onemesh_slave_key_offer()(地址:0x414d14)
tpdp_pkt_parser()#1
首先检查UDP套接字标头大小是否至少为0x10;调用tdpd_get_pkt_len()(地址:0x40d620),该函数返回在包头中声明的包长度,如果数据包长度超过0x410,则此函数返回-1;最后再通过tdpd_pkt_sanity_checks()(地址:0x40c9d0),检查数据包版本(版本字段,数据包中的第一个字节)是否等于1。
接着,使用自定义校验和函数tpdp_pkt_calc_checksum()(地址:0x4037f0)计算数据包的校验和。
由于tpdp_pkt_calc_checksum()内容较多,借助lao_bomb漏洞利用代码的calc_checksum()分析:
lao_bomb漏洞利用代码的calc_checksum()
首先,在数据包的校验和字段中设置魔术变量0x5a6b7c8d,然后使用带有1024个字节的表reference_tbl来计算整个数据包(包括报头)的校验和;校验和通过验证并且所有结果正确之后,tdpd_pkt_sanity_checks()返回0。
tpdp_pkt_parser()#2
漏洞函数onemesh_slave_key_offer():
第一部分:将payload传递给tpapp_aes_decrypt()(地址:0x40b190),功能为使用AES算法和静态密钥“TPONEMESH_Kf!xn?gj6pMAt-wBNV_TDP”解密payload。
第二部分:对onemesh对象做一些设置后,解析payload(一个json对象)获取json键及其值。
第三部分:按顺序处理获取的键与值(若键不存在,直接退出函数),json对象中的值传递给堆栈变量slaveMac、slaveIp等,调用create_csjon_obj()(地址:0x405fe8)函数处理。
第四部分:create_csjon_obj()处理:堆栈变量slaveMac被传递给systemCmd变量,然后由system(systemCmd)执行。(函数太长,只看部分代码)
假设jason对象如下所示:
want_to_join必须为false,type设置为0xf0,opcode设置为6,将flags设置为1,并正确获取校验和字段。
数据包使用AES加密:固定密钥:TPONEMESH_Kf!xn?gj6pMAt-wBNV_TDP,使用CBC模式(IV固定值:1234567890abcdef1234567890abcdef),实际使用其中的128位密钥的AES-CBC,256位密钥和IV中有一半没有使用。
代码执行:
知道了到达漏洞代码的可行路径,发送数据包进行命令执行还需克服两个限制:
1、strncpy()只从slave_mac_info键中的值拷贝0x11 字节(17个字节)递给slaveMac变量,且包括终止字符null。
2、slaveMac变量中的值有单双引号的使用,需要进行转义。
为了转义单双引号,需要添加:';<PAYLOAD>',由此占用了3个字符,实际测试中payload剩下12个字符可用,如此一来几乎不能做什么有意义的命令执行。
解决方法是多次触发漏洞,将命令写入文件最终当作shell脚本执行,例如:
Ø printf a>>z
将字符a追加到文件z当中,此payload用掉了11个字节。另外,shell会将数字解释为文件描述符,特殊字符"."或";"的无法写入;
Ø printf '1'>x
创建一个名为“x”的新文件,文件仅包含字符“1”,此payload用掉了12个字节,无法添加额外的“>” ;
Ø cat x*>>z*
对于数字或特殊字符,首先将字符写入新文件,然后使用cat将新文件的内容附加到正在构建的命令文件。
由于文件z最终会被命名成z”}),因此在每个文件名后添加“*”,shell会使用特殊的'*'字符自动补全。
当前文件创建文件的位置在根目录下,通常嵌入是文件系统的根目录是不可写的,因此写文件需要到/tmp目录下操作,但TP-Link的根文件系统是以读写方式安装,由此节省很多字节(cd tmp)。
最后以root用户执行命令文件:sh z
按照如上思路,exp的构造如下所示,可直接在metasploit中加载执行,同时可以参照代码对漏洞利用原理再次进行理解,有TP-Link Archer路由器设备的情况下可对exp进行测试。
总结整个漏洞利用实现的过程:在理解了数据通信原理和数据包格式的基础下,找到触发漏洞的可行途径,在漏洞执行存在极为苛刻的限制下,通过多次触发漏洞将命令写入文件中,最终当作shell脚本执行,从而达到任意代码执行的目的。
本文参考了相关资料,对ZDI博客进行了理解、基本翻译和相关验证工作,如有不当之处请见谅。
附EXP链接:https://pan.baidu.com/s/102VD7FFtm5ykJ6tlCGQ98w
提取码:yliv
参考资料:
https://www.thezdi.com/blog/2020/4/6/exploiting-the-tp-link-archer-c7-at-pwn2own-tokyo
https://github.com/rapid7/metasploit-framework/
看雪ID:ninjia远方
https://bbs.pediy.com/user-396702.htm
推荐文章++++
好书推荐